package com.morik.util;

import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.StringWriter;
import java.io.Writer;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.lang.reflect.Method;
import java.lang.reflect.Array;

public class JavaScriptBuilder {   
    static Set<Class<?>> simpleTypes = new HashSet<Class<?>>();  
    static Set<Class<?>> ignoredTypes = new HashSet<Class<?>>();
    static {
        simpleTypes.add(Byte.TYPE);
        simpleTypes.add(Short.TYPE);
        simpleTypes.add(Integer.TYPE);
        simpleTypes.add(Long.TYPE);
        simpleTypes.add(Float.TYPE);
        simpleTypes.add(Double.TYPE);
        simpleTypes.add(Boolean.TYPE);
        ignoredTypes.add(Class.class);
    }
  
    private Set<Integer> visitedIdentities = new HashSet<Integer>();   
    private Map<String,String> objectValues = new HashMap<String,String>();    
    private Map<String,String> assignments = new HashMap<String,String>();   
    private Object rootObject;    
    private Set<Class<?>> excludeClasses;

    public JavaScriptBuilder(Object root, Class<?>... userClassesToExclude) {
        this.rootObject = root;
        this.excludeClasses = new HashSet<Class<?>>();

        for (Class<?> type : userClassesToExclude) {
            this.excludeClasses.add(type);
        }
        this.excludeClasses.addAll(ignoredTypes);
    }
   
    public String build() {
        Writer writer = new StringWriter();
        build(writer);
        return writer.toString();
    }
  
    public void build(Writer writer) {
        try {
            // If for some reason a caller provided us with a simple scalar object, then
            // convert it and short-circuit return
            if (isScalarType(this.rootObject)) {
                writer.write(getScalarAsString(this.rootObject));
                writer.write(";\n");
                return;
            }

            String rootName = "_sj_root_" + new Random().nextInt(Integer.MAX_VALUE);
            buildNode(rootName, this.rootObject);

            writer.write("var ");
            writer.write(rootName);
            writer.write(";\n");

            for (Map.Entry<String,String> entry : objectValues.entrySet()) {
                writer.append("var ");
                writer.append(entry.getKey());
                writer.append(" = ");
                writer.append(entry.getValue());
                writer.append(";\n");
            }

            for (Map.Entry<String,String> entry : assignments.entrySet()) {
                writer.append(entry.getKey());
                writer.append(" = ");
                writer.append(entry.getValue());
                writer.append(";\n");
            }

            writer.append(rootName).append(";\n");
        }
        catch (Exception e) {        
        }
    }
   
    public boolean isExcludedType(Class<?> type) {
        for (Class<?> excludedType : this.excludeClasses) {
            if (excludedType.isAssignableFrom(type)) {
                return true;
            }
            else if (type.isArray() && excludedType.isAssignableFrom(type.getComponentType())) {
                return true;
            }
        }

        return false;
    }

   
    public boolean isScalarType(Object in) {
        if (in == null) return true; // Though not strictly scalar, null can be treated as such

        Class type = in.getClass();
        return simpleTypes.contains(type)
            || Number.class.isAssignableFrom(type)
            || String.class.isAssignableFrom(type)
            || Boolean.class.isAssignableFrom(type)
            || Date.class.isAssignableFrom(type);
    }

    public String getScalarAsString(Object in) {
        if (in == null) return "null";

        Class type = in.getClass();

        if (String.class.isAssignableFrom(type)) {
            return quote((String) in);
        }
        else if(Date.class.isAssignableFrom(type)) {
            return "new Date(" + ((Date) in).getTime() + ")";
        }
        else {
            return in.toString();
        }
    }

    
    public static String quote(String string) {
        if (string == null || string.length() == 0) {
            return "\"\"";
        }

        char c = 0;
        int len = string.length();
        StringBuilder sb = new StringBuilder(len + 10);

        sb.append('"');
        for (int i = 0; i < len; ++i) {
            c = string.charAt(i);
            switch (c) {
                case '\\':
                case  '"':
                    sb.append('\\').append(c);
                    break;
                case '\b':
                    sb.append("\\b");
                    break;
                case '\t':
                    sb.append("\\t");
                    break;
                case '\n':
                    sb.append("\\n");
                    break;
                case '\f':
                    sb.append("\\f");
                    break;
                case '\r':
                    sb.append("\\r");
                    break;
                default:
                    if (c < ' ') {
                        // The following takes lower order chars and creates unicode style
                        // char literals for them (e.g. \u00F3)
                        sb.append("\\u");
                        String hex = Integer.toHexString(c);
                        int pad = 4 - hex.length();
                        for (int j=0; j<pad; ++j) {
                            sb.append("0");
                        }
                        sb.append(hex);
                    }
                    else {
                        sb.append(c);
                    }
            }
        }

        sb.append('"');
        return sb.toString();
    }

 
    void buildNode(String name, Object in) throws Exception {
        int systemId = System.identityHashCode(in);
        String targetName = "_sj_" + systemId;

        if (this.visitedIdentities.contains(systemId)) {
            this.assignments.put(name, targetName);
        }
        else if ( isExcludedType(in.getClass()) ) {
            // Do nothing, it's being excluded!!
        }
        else {
            this.visitedIdentities.add(systemId);

            if (Collection.class.isAssignableFrom(in.getClass())) {
                buildCollectionNode(targetName, (Collection) in);
            }
            else if (in.getClass().isArray()) {
                buildArrayNode(targetName, in);
            }
            else if (Map.class.isAssignableFrom(in.getClass())) {
                buildMapNode(targetName, (Map) in);
            }
            else {
                buildObjectNode(targetName, in);
            }

            this.assignments.put(name, targetName);
        }
    }

    
    void buildObjectNode(String targetName, Object in) throws Exception {
        StringBuilder out = new StringBuilder();
        out.append("{");
        PropertyDescriptor[] props = Introspector.getBeanInfo(in.getClass()).getPropertyDescriptors();

        for (PropertyDescriptor property : props) {
            try {
                Method readMethod = property.getReadMethod();
                if (readMethod != null) {
                    Object value = property.getReadMethod().invoke(in);

                    if (isExcludedType(property.getPropertyType()) || value == null) {
                        continue;
                    }

                    if (isScalarType(value)) {
                        if (out.length() > 1) {
                            out.append(", ");
                        }
                        out.append(property.getName());
                        out.append(":");
                        out.append( getScalarAsString(value) );
                    }
                    else {
                        buildNode(targetName + "." + property.getName(), value);
                    }
                }
            }
            catch (Exception e) {
                
            }
        }

        // Do something a little extra for enums
        if (Enum.class.isAssignableFrom(in.getClass())) {
            Enum e = (Enum) in;

            if (out.length() > 1) { out.append(", "); }
            out.append("ordinal:").append( getScalarAsString(e.ordinal()) );
            out.append(", name:").append( getScalarAsString(e.name()) );
        }

        out.append("}");
        this.objectValues.put(targetName, out.toString());
    }

    
    void buildMapNode(String targetName, Map<?,?> in) throws Exception {
        StringBuilder out = new StringBuilder();
        out.append("{");

        for (Map.Entry<?,?> entry : in.entrySet()) {
            String propertyName = getScalarAsString(entry.getKey());
            Object value = entry.getValue();

            if (isScalarType(value)) {
                if (out.length() > 1) {
                    out.append(", ");
                }
                out.append(propertyName);
                out.append(":");
                out.append( getScalarAsString(value) );
            }
            else {
                buildNode(targetName + "[" + propertyName + "]", value);
            }
        }

        out.append("}");
        this.objectValues.put(targetName, out.toString());
    }

  
    void buildArrayNode(String targetName, Object in) throws Exception {
        StringBuilder out = new StringBuilder();
        out.append("[");

        int length = Array.getLength(in);
        for (int i=0; i<length; i++) {
            Object value = Array.get(in, i);

            if (isScalarType(value)) {
                out.append( getScalarAsString(value) );
            }
            else {
                out.append("null");
                buildNode(targetName + "[" + i + "]", value);
            }

            if (i != length-1) {
                out.append(", ");
            }
        }

        out.append("]");
        this.objectValues.put(targetName, out.toString());
    }
  
    void buildCollectionNode(String targetName, Collection in) throws Exception {
        buildArrayNode(targetName, in.toArray());
    }
}
